package com.mayblackcat.amoy.product.service.impl;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.TypeReference;
import org.redisson.api.RLock;
import org.redisson.api.RReadWriteLock;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.stereotype.Service;

import java.io.Serializable;
import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;

import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.mayblackcat.common.utils.PageUtils;
import com.mayblackcat.common.utils.Query;

import com.mayblackcat.amoy.product.dao.CategoryDao;
import com.mayblackcat.amoy.product.entity.CategoryEntity;
import com.mayblackcat.amoy.product.service.CategoryService;
import org.springframework.util.StringUtils;


/**
 * @author mayblackcat
 *
 * 如何保证缓存和数据库中数据一致性：双写模式(修改完数据库，立即对缓存数据进行修改，但是会产生脏数据)和失效模式（改完数据库中数据后清空缓存，也会产生脏数据）
 */
@Service("CategoryService")
public class CategoryServiceImpl extends ServiceImpl<CategoryDao, CategoryEntity> implements CategoryService{

    @Autowired
    private CategoryDao categoryDao;

    /**
     *  redis缓存模板
     */
    @Autowired
    private RedisTemplate redisTemplate;

    /**
    *  redisson框架
    */
    @Autowired
    private RedissonClient redisson;

    @Override
    public PageUtils queryPage(Map<String, Object> params) {
        IPage<CategoryEntity> page = this.page(
                new Query<CategoryEntity>().getPage(params),
                new QueryWrapper<CategoryEntity>()
        );

        return new PageUtils(page);
    }


    /**
     * 将数据处理为三级目录，并排序
     * @Param
     * @author MayBlackCat
     * @return
     */
    @Override
    public List<CategoryEntity> listTree() {
        //查询出所有分类

        List<CategoryEntity> entities = baseMapper.selectList(null);

        //找出所有一级分类
        List<CategoryEntity> parentEntities= entities.stream().
                filter((categoryEntity)->{return categoryEntity.getParentCid()==0;}).
                map((menu)->{menu.setChildren(getChildCategory(entities,menu)); return menu;}).
                sorted((category1,category2)->{return (int) ((category1.getCatId()==null?0:category1.getCatId())-(category2.getCatId()==null?0:category2.getCatId()));}).
                collect(Collectors.toList());


        return parentEntities;
    }

    @Override
    public void removeMenuByIds(List<Long> asList) {
        // TODO 检查当前节点是否被别的地方引用
        baseMapper.deleteBatchIds(asList);
    }

    /**
     * 获取子分类
     * @Param
     * @author MayBlackCat
     * @return
     */
    public List<CategoryEntity> getChildCategory(List<CategoryEntity> all,CategoryEntity parent){


        //找出父级分类所有的子分类
        List<CategoryEntity> child=new ArrayList<>();
        for (CategoryEntity category: all) {
            if(category.getParentCid()==0) {
                continue;
            }
            if (category.getParentCid().equals(parent.getCatId())){
                parent.setChildren(child);
                child.add(category);

                getChildCategory(all,category);
            }


        }

        //排序
        Collections.sort(child,(o1, o2) ->{return  (o1.getSort()==null?0:o1.getSort())-(o2.getSort()==null?0:o2.getSort()); });
        return  child;

    }


    /**
     * 使用redis缓存，减少重复查询
     *
     * 压力测试后：产生堆外内存溢出:outOfDirectMemoryError
     * 1)、springboot2.0以后默认使用lettuce作为操作redis的客户端。它使用netty进行网络通信。
     * 2)、lettuce的bug导致netty堆外内存溢出-Xmx300m; netty如果没有指定堆外内存，默认使用-Xmx300m
     * 可以通过-Dio.netty.maxDirectMemory进行设置
     * 解决方案:不能使用-Dio.netty.maxDirectMemory只去调大堆外内存。
     * 1)、升级Lettuce客户端。2)、切换使用jedis
     * redisTemplate:
     *      lettuce、jedis操作redis的底层客户端。Spring再次封装redisTemplate;
     *
     * 在高并发场景下：
     * 当大量请求查询缓存没有数据且数据库中也没有数据，会导致缓存穿透。简单解决：加null值
     * 大量缓存数据大面积同时失效，导致缓存雪崩。简单解决：设置随机失效时间
     * 某热点数据失效，大量请求同时进去查数据库，导致缓存击穿。简单解决：加锁
     *
     * 由于spring在容器中是单实例的，因此可以通过锁住本地服务，但在分布式场景下，会有多个服务器，因此需要分布式锁。
     *
     * 本地锁:直接在方法上加synchronized
    */
    public  Map<String,List<CategoryEntity>> getCatalogJson() {
        //给缓存中放json字符串，拿出的json字符串，还用逆转为能用的对象类型，【序列化与反序列化】
        //1、加入缓存逻辑,缓存中存的数据是json字符串。
        //JSON跨语言，跨平台兼容。

        String catalogJSON = (String) redisTemplate.opsForValue().get("catalogJSON");


        if (StringUtils.isEmpty(catalogJSON)) {
            //2、缓存中没有,查询数据库
            Map<String, List<CategoryEntity>> catalogJsonFromDb = new HashMap<>();
            catalogJsonFromDb.put("catalogJSON", listTree());
            //3、查到的数据再放入缓存，将对象转为json放在缓存中
            String s = JSON.toJSONString(catalogJsonFromDb);
            redisTemplate.opsForValue().set("catalogJSON", s);
            return catalogJsonFromDb;
        }


        //转为我们指定的对象。
        Map<String, List<CategoryEntity>> result = JSON.parseObject(catalogJSON, new TypeReference<Map<String, List<CategoryEntity>>>() {
        });
        return result;
    }

    /**
     *  分布式锁
     *  redis中set key value NX用于设置分布式锁，但是为了消除死锁现象，我们需要为持锁者拥有锁过期时间。
     *  为了保证设置过期时间和设置锁是原子操作，redis对应有set key value EX XX NX
     *  redis除了加锁需要保证原子性，解锁也需要保证原子性
     *
    */
    public Map<String,List<CategoryEntity>> getRedisLockCatalogJson(){
        //每次进入前先查询缓存，当缓存有时就不必进行加锁操作
        String catalogJSON = (String) redisTemplate.opsForValue().get("catalogJSON");
        if (!StringUtils.isEmpty(catalogJSON)){
            //转为我们指定的对象。
            Map<String, List<CategoryEntity>> result = JSON.parseObject(catalogJSON, new TypeReference<Map<String, List<CategoryEntity>>>() {
            });
            return result;
        }

            //设置uuid保证删除锁时，删除的是该线程持有的
        String uuid=UUID.randomUUID().toString();
        //设置锁和过期时间
        Boolean redisLock=redisTemplate.opsForValue().setIfAbsent("lock",uuid,2000, TimeUnit.MILLISECONDS);
        if(redisLock){
            Map<String, List<CategoryEntity>> dataFromDb=null;
            try {
                dataFromDb=getCatalogJson();
            }catch (Exception e){
                e.printStackTrace();
            }finally {
                // 删除也必须是原子操作 Lua脚本操作 删除成功返回1 否则返回0
                String script = "if redis.call('get',KEYS[1]) == ARGV[1] then return redis.call('del',KEYS[1]) else return 0 end";
                // 原子删锁
                redisTemplate.execute(new DefaultRedisScript<>(script, Long.class), Arrays.asList("lock"), uuid);
            }
            return dataFromDb;

        }else {

            //睡2秒后进行重试
            try {
                TimeUnit.SECONDS.sleep(2);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            return getRedisLockCatalogJson();
        }

    }

    /**
     * 如何为业务时间长的代码，锁时间自动续期呢？
     * 通过redisson解决
     * 1.redisson可以为锁自动续期(默认加锁和续锁为30s)，且不需要手动释放锁。（看门狗机制）
     *
     *
    */
    public Map<String,List<CategoryEntity>> getRedissonLockCatalogJson() {
        //获取读写锁
        //当有写线程时，所有读线程都需要停下等待写线程完成后在进行读线程。而只有读线程时等同于无锁状态
        RReadWriteLock rwLock=redisson.getReadWriteLock("rwLock");
        //获取可重入锁
        RLock lock=redisson.getLock("myLock");
        //有看门狗，每次隔10s自动续期30s
        lock.lock();
        //指定锁的失效时间,指定后不会自动续期
        lock.lock(10,TimeUnit.SECONDS);
        try {
            System.out.println("执行业务");

        }catch (Exception e){
            e.printStackTrace();
        }finally {
            //释放锁
            lock.unlock();
        }
        Map<String,List<CategoryEntity>> map=new HashMap<>();
        return map;
    }
}